/*
 * File     : PrimitiveParameter.java
 * Created  : 22 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.call;

import static org.objectweb.asm.Opcodes.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.googlecode.dni.internal.structure.StructureBuilder;
import com.googlecode.dni.type.nativetype.SizeT;
import com.googlecode.dni.type.nativetype.Unsigned;
import com.googlecode.dni.type.nativetype.WCharT;

/**
 * <p>
 *  Encapsulates a parameter to a native call that comes from a Java primitive.
 * </p>
 *
 * @author Matthew Wilson
 */
final class PrimitiveParameter extends Parameter
{

    /** The annotation type for a natively sized type. */
    private Class< ? > nativeAnnotationType;

    private boolean unsigned;


    /**
     * Creates a new instance for a return parameter.
     *
     * @param method
     *            the method
     */
    PrimitiveParameter( final Method method )
    {
        this( method, method.getReturnType(), method.getAnnotations() );

        // wchar_t and sizr_t have to be unsigned; others are optional
        this.unsigned = ( this.nativeAnnotationType == WCharT.class || this.nativeAnnotationType == SizeT.class )
                        || method.getAnnotation( Unsigned.class ) != null;
    }

    /**
     * Creates a new instance for a vanilla parameter.
     *
     * @param method
     *            the owning method
     * @param parameterType
     *            the parameter type
     * @param annotations
     *            the annotations on the parameter
     */
    PrimitiveParameter( final Method method, final Class< ? > parameterType, final Annotation[] annotations )
    {
        setJavaType( parameterType );

        for ( Annotation annotation : annotations )
        {
            Class< ? extends Annotation > annotationType = annotation.annotationType();
            Character signature = NativeCallFactory.NATIVE_SIGNATURES.get( annotationType );

            if ( signature != null )
            {
                setTypeFromAnnotation( method, parameterType, annotationType );
            }
        }

        if ( this.nativeAnnotationType == null )
        {
            setNativeType( NativeCallFactory.NATIVE_SIGNATURES.get( parameterType ) );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    int getJavaVarSlots()
    {
        if ( getJavaType() == long.class || getJavaType() == double.class )
        {
            // in the byte-code, longs and doubles take two slots
            return 2;
        }

        return 1;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeGetParameter( final MethodVisitor methodVisitor )
    {
        if ( this.nativeAnnotationType != null )
        {
            if ( getJavaType() == int.class )
            {
                // - Java declared type is int
                // - Destination size must be <= 32 bits
                // - Java treats <= 32 bits as ints in byte-code
                methodVisitor.visitVarInsn( ILOAD, getJavaVarIndex() );
            }
            else
            {
                assert getJavaType() == long.class;

                // - Java declared type is long
                // - Destination size is either <= 32 bits or 64 bits
                // - Have to narrow as appropriate

                if ( getNativeType() == 'J' )
                {
                    methodVisitor.visitVarInsn( LLOAD, getJavaVarIndex() );
                }
                else
                {
                    // - Java treats <= 32 bits as ints in byte-code
                    methodVisitor.visitVarInsn( LLOAD, getJavaVarIndex() );
                    methodVisitor.visitInsn( L2I );
                }
            }
        }
        else
        {
            switch ( getNativeType() )
            {
                case 'Z':
                case 'B':
                case 'S':
                case 'C':
                case 'I':
                    methodVisitor.visitVarInsn( Opcodes.ILOAD, getJavaVarIndex() );
                    break;

                case 'J':
                    methodVisitor.visitVarInsn( Opcodes.LLOAD, getJavaVarIndex() );
                    break;

                case 'F':
                    methodVisitor.visitVarInsn( Opcodes.FLOAD, getJavaVarIndex() );
                    break;

                case 'D':
                    methodVisitor.visitVarInsn( Opcodes.DLOAD, getJavaVarIndex() );
                    break;

                default:
                    throw new AssertionError( getNativeType() );
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeReturnParameter( final MethodVisitor methodVisitor,
                               final int returnVarIndex )
    {
        writeGetReturnResult( methodVisitor, returnVarIndex );

        if ( this.nativeAnnotationType != null )
        {
            if ( getJavaType() == int.class )
            {
                // - Java declared type is int
                // - The native size must be <= 32 bits
                // - Java treats <= 32 bits as ints in byte-code

                if ( this.unsigned )
                {
                    // for unsigned, we have to extend in an unsigned manner
                    switch ( getNativeType() )
                    {
                        case 'B':
                            StructureBuilder.writeConstantInt( methodVisitor, 0xff );
                            methodVisitor.visitInsn( IAND );
                            break;

                        case 'C':
                        case 'S':
                            StructureBuilder.writeConstantInt( methodVisitor, 0xffff );
                            break;

                        case 'I':
                            // no widening required
                            break;

                        default:
                            throw new AssertionError( getNativeType() );
                    }
                }

                methodVisitor.visitInsn( Opcodes.IRETURN );
            }
            else
            {
                assert getJavaType() == long.class;

                // - Java declared type is long
                // - The native size is either <= 32 bits or 64 bits
                // - Have to widen as appropriate

                if ( getNativeType() != 'J' )
                {
                    // - Java treats <= 32 bits as ints in byte-code
                    methodVisitor.visitInsn( I2L );

                    if ( this.unsigned )
                    {
                        // for unsigned, we have to extend in an unsigned manner
                        assert getNativeType() == 'I' : "Weird size for size_t or long!";
                        methodVisitor.visitLdcInsn( 0xffffffffL );
                        methodVisitor.visitInsn( LAND );
                    }
                }

                methodVisitor.visitInsn( Opcodes.LRETURN );
            }
        }
        else
        {
            switch ( getNativeType() )
            {
                case 'Z':
                case 'B':
                case 'S':
                case 'C':
                case 'I':
                    methodVisitor.visitInsn( Opcodes.IRETURN );
                    break;

                case 'J':
                    methodVisitor.visitInsn( Opcodes.LRETURN );
                    break;

                case 'F':
                    methodVisitor.visitInsn( Opcodes.FRETURN );
                    break;

                case 'D':
                    methodVisitor.visitInsn( Opcodes.DRETURN );
                    break;

                default:
                    throw new AssertionError( getNativeType() );
            }
        }
    }

    /**
     * Sets the native type from the annotation <code>NativeXxx</code> type.
     *
     * @param method
     *            the method
     * @param parameterType
     *            the parameter type
     * @param annotationType
     *            the annotation type
     */
    private void setTypeFromAnnotation( final Method method,
                                        final Class< ? > parameterType,
                                        final Class< ? extends Annotation > annotationType )
    {
        if ( this.nativeAnnotationType != null )
        {
            throw new IllegalArgumentException( "Multiple NativeXxx annotations not permitted: " + method );
        }

        // what declared type is required?
        Class< ? > javaType = NativeCallFactory.NATIVE_TYPES.get( annotationType );

        if ( parameterType != javaType )
        {
            throw new IllegalArgumentException( annotationType.getSimpleName() + " requires Java type of " + javaType.getName()
                                                + ": " + method );
        }

        this.nativeAnnotationType = annotationType;

        setNativeType( NativeCallFactory.NATIVE_SIGNATURES.get( annotationType ) );
    }

}
